面试整理——Dubbo

Dubbo

一. 基础概念

问:什么是服务治理?问:为什么需要服务治理?

  • 什么是服务治理?
    • 简单来说,就是管理微服务,确保平台整体正常、平稳地运行。服务治理是在微服务架构中,为了保证系统整体的稳定性、可靠性、高效性,对微服务进行管理和控制的一系列机制。
  • 为什么需要服务治理?
    • 微服务数量庞大: 微服务架构将一个大型应用拆分成许多小的服务,这些服务之间相互依赖,管理起来非常复杂。
    • 服务间通信频繁: 微服务之间通过网络进行通信,这增加了系统的不稳定性,可能出现网络故障、服务宕机等问题。
    • 服务动态变化: 微服务可以独立部署、升级,这使得系统的状态变得动态多变,增加了管理难度。
  • 服务治理的核心内容:
    • 服务注册与发现: 服务启动时将自己注册到服务注册中心,其他服务可以通过服务注册中心找到需要调用的服务。
    • 配置中心: 集中管理所有微服务的配置信息,实现配置的动态变更和实时生效。
    • 负载均衡: 将请求均衡地分发到多个服务实例上,提高系统的并发处理能力。
    • 服务熔断: 当某个服务出现故障时,快速隔离该服务,防止故障扩散。
    • 服务降级: 在系统负载过高时,对非核心服务进行降级,保证核心服务的可用性。
    • 流量控制: 对系统流量进行限制,防止系统过载。
    • 链路追踪: 追踪请求在整个系统中的调用链路,方便定位问题。
    • 日志收集与分析: 收集和分析系统日志,以便监控系统运行状态。
  • 常用的服务治理框架:
    • Spring Cloud: 一个基于 Spring Boot 的微服务开发框架,提供了全面的服务治理解决方案。
    • Dubbo: 阿里巴巴开源的分布式服务框架,具有高性能、高可用性等特点。
    • Istio: 一个服务网格,提供流量管理、安全、策略执行等功能。

问:什么是RPC?为什么要用RPC,它与HTTP有什么关系吗?

  • 什么是RPC?

    • RPC,全称 Remote Procedure Call,即 远程过程调用。它是一种计算机通信协议,可以让运行在一台计算机上的程序调用另一台计算机上的程序,就好像调用本地程序一样。
  • 核心思想是 透明化远程调用。也就是说,调用方不需要关心底层的网络通信细节,只需要像调用本地方法一样调用远程方法即可。这大大简化了分布式系统的开发。

  • 工作流程:

    1. 客户端发起调用: 客户端调用本地 stub(桩)上的方法。
    2. stub 将调用信息打包: stub 会将方法名、参数等信息打包成一个请求。
    3. 网络传输: 请求通过网络发送到服务端。
    4. 服务端接收请求: 服务端的 stub 接收到请求,将请求解包。
    5. 服务端执行方法: 服务端 stub 调用实际的服务方法,并获取结果。
    6. 结果返回: 结果通过网络返回给客户端。
    7. 客户端处理结果: 客户端 stub 接收到结果,并返回给客户端调用方。
  • 优势:

    • 简化开发: 将分布式系统调用转化为本地调用,降低开发复杂度。
    • 提高效率: RPC 框架通常会提供一些优化措施,如连接池、序列化/反序列化、异步调用等,提高系统性能。
    • 促进模块化: 不同的服务可以独立开发、部署和维护,提高系统的可维护性。
  • 应用场景:
    • 微服务架构: RPC 是微服务架构中服务之间通信的核心技术。
    • 分布式系统: RPC 可以用于构建大型分布式系统。
    • 云计算: 云计算平台通常提供 RPC 服务,方便用户构建云应用。
  • RPC 框架:
    • gRPC: 基于 Protocol Buffers 的高性能、通用开源 RPC 框架。
    • Dubbo: 阿里巴巴开源的高性能、轻量级的 Java RPC 框架。
    • Thrift: Facebook 开源的跨语言 RPC 框架。
  • 为什么要用RPC,它与HTTP有什么关系吗?
    • HTTP是传输协议,而RPC是相对于本地调用的远程调用概念,用于分布式系统之间的通信,可以用HTTP,也可以使用HTTP/2、gRPC、TCP 等所有主流通信协议。

问:什么是Dubbo?基本用法?主要应用场景?Dubbo和SpringCloud的关系?

官网:Apache Dubbo

  • 什么是Dubbo?

    • Apache Dubbo 是一款易用、高性能的 WEB 和 RPC 框架,同时为构建企业级微服务提供服务发现、流量治理、可观测、认证鉴权等能力、工具与最佳实践。
  • Dubbo的核心特性?

    • 微服务编程范式和工具
    • 服务发现
    • 动态配置
    • 负载均衡
    • 流量管控:控制服务间的流量走向和 API 调用,实现在运行期动态的调整服务行为如超时时间、重试次数、限流参数等,通过控制流量分布可以实现 A/B 测试、金丝雀发布、多版本按比例流量分配、条件匹配路由、黑白名单等,提高系统稳定性。
      • 动态调整超时时间
      • 服务重试
      • 访问日志
      • 同区域优先
      • 灰度环境隔离
      • 参数路由
      • 按权重比例分流
      • 金丝雀发布
      • 服务降级
      • 实例临时拉黑
      • 指定机器导流
    • 认证鉴权
    • 通信协议
    • 扩展适配
    • 服务网格
  • Dubbo的角色有?

    • Consumer:需要调用远程服务的服务消费方
    • Registry:注册中心
    • Provider:服务提供方
    • Container:服务运行的容器
    • Monitor:监控中心
  • 为什么要用Dubbo?与Spring Cloud的关系?

    • 使用微服务架构来搭建应用时,需要解决服务拆分与定义、数据通信、地址发现、流量管理、数据一致性、系统容错能力等一系列问题。Dubbo提供了如RPC 通信解决服务之间的通信问题、服务监控和治理等功能
    • Dubbo与Spring Cloud提供的服务类似,二者可以理解为竞品。Dubbo 不绑定特定的通信协议,支持 HTTP、HTTP/2、gRPC、TCP 等所有主流通信协议,支持 Dubbo2、Triple 两款高性能通信协议。
  • 基本使用:

    • 通过接口定义服务:

      1
      2
      3
      public interface DemoService {
      String hello(String arg);
      }
    • 服务提供者实现接口:

      1
      2
      3
      4
      5
      6
      @DubboService
      public class DemoServiceImpl implements DemoService {
      public String hello(String arg) {
      // put your microservice logic here
      }
      }
    • 服务提供者首先要将服务定义以 Jar 包形式发布到 Maven 中央仓库。补充 Dubbo 配置并启动 Dubbo Server。

      1
      2
      3
      4
      5
      6
      7
      8
      dubbo:
      application:
      name: dubbo-demo
      protocol:
      name: dubbo
      port: -1
      registry:
      address: zookeeper://127.0.0.1:2181
    • 消费方通过 Maven/Gradle 引入 DemoService 服务定义依赖。

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-demo-interface</artifactId>
      <version>3.2.0</version>
      </dependency>
    • 编程注入远程 Dubbo 服务实例。

      1
      2
      3
      4
      5
      @Bean
      public class Consumer {
      @DubboReference
      private DemoService demoService;
      }

问: Dubbo的核心功能有哪些?

Dubbo 提供了强大的 分布式服务治理 能力,核心功能包括:

1. 透明化的 RPC 远程调用

  • 功能:让远程调用像本地调用一样简单。

  • 实现方式:

    • 通过 动态代理 生成远程调用对象(Stub)。
    • Consumer 调用 Invoker,通过 网络协议 传输请求到 Provider。
  • 示例:

    1
    2
    3
    4
    @DubboReference
    private OrderService orderService;

    orderService.createOrder("123");

2. 负载均衡

  • 功能:支持多种 负载均衡算法,提升性能。

  • 常见策略:

    • Random(随机,默认)
    • RoundRobin(轮询)
    • LeastActive(最少活跃)
    • ConsistentHash(一致性哈希)
  • 示例

    (Consumer 侧配置负载均衡):

    1
    2
    3
    dubbo:
    consumer:
    loadbalance: leastactive

3. 服务注册与发现

  • 功能:支持 动态扩容、故障剔除,无需手动管理服务地址。

  • 工作方式:

    • Provider 启动时 自动注册服务
    • Consumer 订阅服务,实时获取可用 Provider。
  • 示例(Zookeeper 注册中心):

    1
    2
    3
    dubbo:
    registry:
    address: zookeeper://127.0.0.1:2181

4. 服务容错机制

  • 功能:防止单点故障,提供 多种降级策略

  • 支持的容错策略:

    • Failover(失败自动切换,默认)
    • Failfast(快速失败)
    • Failsafe(失败安全,忽略异常)
    • Failback(失败自动恢复)
  • 示例(配置 Failfast 策略):

    1
    2
    3
    4
    dubbo:
    reference:
    retries: 0 # 失败时不重试
    cluster: failfast

5. 服务路由与动态配置

  • 功能:通过 Router 规则 进行动态流量管理。

  • 常见应用:

    • 灰度发布(部分用户使用新版本服务)。
    • IP、区域流量限制。
  • 示例(灰度规则:部分用户请求新服务):

    1
    2
    conditions:
    - method=pay => tag=gray

6. 服务治理(监控 & 统计)

  • 功能:提供 调用监控、流量分析,支持 Prometheus/Grafana。

  • 支持的监控方式:

    • Dubbo Admin(官方管理平台)。
    • Metrics 监控(Prometheus、Grafana)。
  • 示例(开启监控):

    1
    2
    3
    dubbo:
    metrics:
    protocol: prometheus

7. 多协议 & 多语言支持

  • 支持多种协议(Dubbo、HTTP、gRPC 等)。

  • 支持 Java、Go、Node.js 等多语言调用。

  • 示例(使用 HTTP 协议):

    1
    2
    3
    4
    dubbo:
    protocol:
    name: rest
    port: 8080

Dubbo 核心功能:

  1. RPC 远程调用
  2. 负载均衡
  3. 服务注册 & 发现
  4. 容错机制
  5. 服务路由 & 动态配置
  6. 监控 & 统计
  7. 多协议 & 多语言支持

Dubbo 通过服务治理和高性能 RPC 调用,广泛用于 互联网、电商、金融等高并发分布式系统

问:Dubbo的架构设计?一共划分了哪些层?

Dubbo 通过 分层架构 实现了服务的 解耦、灵活性、可扩展性。每一层都有明确的职责,便于维护和扩展:

  • Consumer 负责调用;
  • Provider 负责实现并暴露服务;
  • Registry 负责服务发现与管理;
  • Protocol 负责数据传输;
  • Admin 提供监控与管理能力。
层级 作用
用户应用层(Consumer) 发起远程调用,配置服务注册与发现。
Dubbo 客户端层(Consumer) 请求发送、负载均衡、容错、协议支持等功能。
网络传输层(Protocol) 实现协议通信,序列化、反序列化,数据传输等功能。
服务注册与发现层(Registry) 管理服务注册、发现、动态变更等功能。
服务提供者层(Provider) 实现服务,暴露服务,注册到注册中心。
服务管理层(Admin) 提供服务治理、监控、配置管理等功能。

整体架构图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+-----------------------------------+
| 用户应用层(Consumer) |
| -----> 远程调用,配置服务注册等 |
+-----------------------------------+
|
|
+-----------------------------------+
| Dubbo 客户端(Consumer) |
| -----> 请求发送,负载均衡,容错等 |
+-----------------------------------+
|
|
+-----------------------------------+
| 网络传输层(Protocol) |
| -----> 实现网络传输(序列化/反序列化) |
+-----------------------------------+
|
|
+-----------------------------------+
| 服务注册与发现层(Registry) |
| -----> 服务注册,订阅,心跳,通知 |
+-----------------------------------+
|
|
+-----------------------------------+
| 服务提供者(Provider) |
| -----> 业务逻辑处理,服务暴露 |
+-----------------------------------+
|
|
+-----------------------------------+
| 服务管理层(Admin) |
| -----> 提供管理与监控功能 |
+-----------------------------------+

Dubbo 的主要分层

1. 用户应用层(Consumer)

  • 作用
    这一层是 Dubbo 的使用方,主要是消费远程服务的应用程序。

  • 功能

    • 发起远程调用,向 Dubbo 服务注册中心请求服务。
    • 配置 @DubboReference<dubbo:reference> 注解,实现服务引入。
    • 使用服务代理对象,进行本地方法调用,Dubbo 会自动将其转化为远程调用。
  • 示例

    1
    2
    3
    4
    5
    6
    @DubboReference
    private UserService userService;

    public void getUserInfo() {
    System.out.println(userService.getUserInfo("123"));
    }

2. Dubbo 客户端层(Consumer)

  • 作用
    这一层负责客户端的 远程调用,它将消费方请求与提供方的服务调用连接起来,处理服务请求的过程。
  • 功能
    • 请求处理:负责向服务提供者发起请求,处理请求和响应。
    • 负载均衡:通过不同的负载均衡策略(如随机、轮询等),选择一个合适的服务实例进行调用。
    • 容错与重试:在请求失败时,根据配置进行容错和重试。
    • 协议支持:支持多种协议(如 Dubbo、HTTP、gRPC 等)。
  • 示例
    • 请求使用 @DubboReference 引用远程服务,客户端自动生成代理对象。

3. 网络传输层(Protocol)

  • 作用
    这一层主要负责实现 RPC 协议的网络通信,包括 序列化与反序列化,请求的编解码,数据传输等。
  • 功能
    • 协议支持:支持多种协议(如 Dubbo、RMI、Hessian、HTTP、Thrift)。
    • 请求序列化:客户端请求数据进行序列化,服务端接收后反序列化。
    • 数据传输:通过 Netty 等网络库进行数据传输。
  • 示例
    • Dubbo 协议:高性能的网络传输协议,基于 Netty。
    • 序列化:支持 JSON、Hessian 等多种序列化方式。

4. 服务注册与发现层(Registry)

  • 作用
    这一层负责管理服务的注册与发现,确保消费者可以找到可用的服务提供者(Provider)。

  • 功能

    • 服务注册:服务提供者启动时将自己暴露的服务(如接口、地址)注册到 注册中心
    • 服务发现:服务消费者从注册中心订阅所需服务,获取可用的服务提供者列表。
    • 动态管理:注册中心会根据服务的健康状态自动推送服务变化,如新增、下线、负载均衡策略等。
  • 常见注册中心

    • Zookeeper(默认)
    • Nacos
    • Etcd
    • Redis
  • 示例

    1
    2
    3
    dubbo:
    registry:
    address: zookeeper://127.0.0.1:2181

5. 服务提供者层(Provider)

  • 作用
    服务提供者实现远程服务接口,并将其注册到注册中心。

  • 功能

    • 服务实现:服务提供者实现具体的业务逻辑。
    • 服务暴露:通过 @DubboService 或 XML 配置,将服务暴露给外部系统。
    • 服务注册:将服务的信息(如接口、地址、端口)注册到 注册中心
  • 示例

    1
    2
    3
    4
    5
    6
    7
    @DubboService
    public class UserServiceImpl implements UserService {
    @Override
    public String getUserInfo(String userId) {
    return "User: " + userId;
    }
    }

6. 服务管理层(Admin)

  • 作用
    这一层提供 服务治理与监控管理,包括服务的配置管理、调用监控、服务降级、流量控制等功能。
  • 功能
    • 服务治理:支持服务的版本管理、灰度发布、流量控制、限流等。
    • 监控统计:实时监控服务调用状态、性能指标(如吞吐量、响应时间、错误率等)。
    • 配置管理:支持动态配置、服务切换等。
  • 示例
    • 使用 Dubbo Admin 提供服务治理和监控功能。

7. 其他辅助层

除了上述主要的架构层,Dubbo 还包括一些辅助功能组件,如 过滤器(Filter)拦截器(Interceptor)回调机制(Callback) 等,它们用于增强 Dubbo 的功能和灵活性。

二. Dubbo的流程-服务暴露、服务引用、服务调用

问:dubbo流程?

Dubbo 作为分布式服务框架,核心流程分为 服务暴露(服务注册)服务引入(服务订阅)服务调用 三个阶段:

  1. 服务暴露:Provider 将服务注册到注册中心,并开启本地服务。
  2. 服务引入:Consumer 从注册中心订阅服务,动态获取 Provider 列表,并创建代理对象。
  3. 服务调用:Consumer 通过代理对象发起远程调用,经过负载均衡、容错等机制最终调用 Provider。

关键设计机制

  1. 动态代理:屏蔽远程调用细节,使 Consumer 像调用本地方法一样使用远程服务。
  2. 集群容错:提供 Failover(默认)、Failfast 等策略,保障调用可靠性。
  3. 异步调用:支持 CompletableFuture 实现非阻塞调用。
  4. SPI 扩展:所有核心组件(Protocol、Cluster、LoadBalance)支持 SPI 扩展。
1
2
Consumer 调用流程:
Proxy -> Filter -> Cluster(LoadBalance) -> Invoker -> Netty -> Provider
  • dubbo的基本流程:
    1. 服务提供者 Provider 启动然后向注册中心注册自己所能提供的服务。
    2. 服务消费者 Consumer 启动向注册中心订阅自己所需的服务。
    3. 注册中心将提供者元信息通知给 Consumer
    4. Consumer 因为已经从注册中心获取提供者的地址,因此可以通过负载均衡选择一个 Provider 直接调用
    5. 之后服务提供方元数据变更的话注册中心会把变更推送给服务消费者
    6. 服务提供者和消费者都会在内存中记录着调用的次数和时间,然后定时的发送统计数据到监控中心。
  • 从角色角度来看:
    • Provider:
      1. 初始化服务:初始化服务并配置相关信息。
      2. Proxy -> Protocol:通过 Proxy 组件根据具体的协议 Protocol 将需要暴露出去的接口封装成 Invoker
      3. Invoker:Invoker 是实际执行方法调用的对象,它将请求传递到具体的服务实现类。
      4. Export -> 注册中心#Registry:将 Exporter 通过 Registry 注册到注册中心,以上为是服务暴露过程。
      5. ThreadPool:处理客户端请求时,服务提供者通过线程池管理并发请求,提供更好的性能。某个线程会根据请求找到对应的 Exporter ,而找到 Exporter 其实就是找到了 Invoker
      6. Server:负责监听客户端请求,并将请求分发给对应的 Invoker 进行处理。
      7. Exporter:Exporter 是服务的封装器,将服务暴露出去,供外部调用。
      8. Filter:过滤器链,在调用前或调用后进行请求处理,如日志记录、权限校验等。经过一层层过滤链之后最终调用实现类然后原路返回结果。
      9. Invoker
    • 注册中心:
      1. Registry:注册中心的作用是维护所有服务提供者的信息,并为消费者提供服务发现的功能。消费者通过注册中心获取服务提供者的地址和协议信息。
    • 网络层:
      1. Codec :编解码器负责处理请求和响应的序列化与反序列化,确保数据在网络中传输时格式正确。
      2. Serialization -> Provider#ThreadPool:负责将请求数据序列化,并通过网络传输到服务提供者。进行 Codec 协议处理,然后反序列化后将请求扔到线程池处理。
    • Consumer:
      1. 初始化服务:消费者初始化并配置要调用的远程服务。
      2. Proxy:消费者持有服务的 Proxy,对应具体的 Invoker,消费者通过 Proxy 调用远程服务。Proxy 持有一个 Invoker 对象
      3. Invoker:Invoker 是消费端调用远程服务的接口,消费者通过它发起请求。
      4. Cluster -> Directory -> Router:Cluster 会从 Directory 获取所有可用的 Invoker,并通过路由规则选择合适的 Invoker。如果有路由规则(比如分区路由),会过滤 Invoker。通过 Cluster 先从 Directory 获取所有可调用的远程服务的 Invoker 列表,如果配置了某些路由规则,比如某个接口只能调用某个节点的那就再过滤一遍 Invoker 列表。
      5. LoadBalance:负载均衡选择一个 Invoker,确保请求均匀地分配到服务提供者。剩下的 Invoker 再通过 LoadBalance 做负载均衡选取一个。
      6. Filter:过滤器链用于在服务调用之前或之后执行逻辑,比如请求日志、请求拦截等。
      7. Invoker -> Client -> 网络层#Codec:通过客户端发送请求,通过网络层的协议处理(如 Netty)传输数据,经过编解码器(Codec)构造请求并序列化,最终发往服务提供者。通过 Client 做数据传输,比如用 Netty 来传输。传输需要经过 Codec 接口做协议构造,再序列化。最终发往对应的服务提供者。

问:服务暴露的流程?Dubbo的服务注册基本原理是?

  1. 检测配置,拼接URL。 Spring IOC 容器刷新完毕之后,会根据配置参数组装成 URL。若支持多注册中心、多协议,则需要分别暴露注册。
  2. 暴露服务,到本地和远程。
    • 本地暴露通过 injvm 协议,通过 proxyFactory.getInvoker 利用 javassist 来进行动态代理,通过protocol.export生成代理类,代理类根据Invoker获取具体协议,利用 javassist 来进行动态代理。通过 Dubbo SPI 机制选择对应的实现类进行 export,而这个方法就会调用 InjvmProtocol#export 方法。使用 InjvmProtocol 暴露服务,避免网络开销,仅用于同一 JVM 内的服务引用。
    • 同样封装为Invoker,若是首次暴露,则调用生成Server如默认的NettyServer,进行一些Handler处理,如心跳机制。将 export 得到的 exporter 存入一个 Map 中,供之后的远程调用查找。登记服务提供者,获取注册中心的URL,并向中心注册。使用 DubboProtocol(默认)或其他协议(如 HTTP),通过 Netty 开启端口监听,等待 Consumer 请求。
  3. 注册服务,到注册中心。将服务元数据(接口名、IP、端口、权重等)注册到 Zookeeper(或其他注册中心),创建临时节点(服务下线时自动删除)。
  4. 启动长连接
    • 启动 Netty 服务端,监听 Consumer 的请求。

问:服务引入的流程?

  • 服务的引入时机:饿汉式和懒汉式。
    • 饿汉式就是加载完毕就会引入,可通过配置 dubbo:reference 的 init 属性开启。通过实现 Spring 的InitializingBean接口中的 afterPropertiesSet方法,容器通过调用 ReferenceBeanafterPropertiesSet方法时引入服务。
    • 懒汉式是只有当这个服务被注入到其他类中时启动引入流程,默认是懒汉式。会先根据配置参数组装成 URL ,一般而言我们都会配置的注册中心,所以会构建 RegistryDirectory 向注册中心注册消费者的信息,并且订阅提供者、配置、路由等节点。
  • 服务的引用方式:本地引入、直连远程服务、通过注册中心引入远程服务
    • 存在一个服务端既是 Provider 又是 Consumer 的情况,当其调用自己的服务时通过本地引用避免网络消耗。
    • 开发测试阶段使用,不需要启动注册中心,Consumer 配置写死Provider 地址直连。
    • Consumer 通过注册中心得知 Provider 的相关信息,然后进行服务的引入,这里还包括多注册中心,同一个服务多个提供者的情况,如何抉择如何封装,如何进行负载均衡、容错并且让使用者无感知。
  • 默认是懒汉式的,服务引入的入口是 ReferenceBean 的 getObject 方法。如果当前还没有这个引用那么就执行 init 方法。init检查配置将其构建成 map 。
  • 然后通过createProxy构建代理,检查是否本地引入。远程的话分析URL判断点对点直连还是注册中心。得到最终的URL。
  • 通过 URL 上的协议利用自适应扩展机制调用对应的 protocol.refer 得到相应的 invoker 。如协议是 registry 因此走的是 RegistryProtocol#refer,获取注册中心对象Registry后,生成URL对象,调用registry.register注册Consumer对象,通过RegistryDirectory#subscribe(负责注册中心的监听)订阅注册中心的Providers目录。最后通过Cluster封装成invoker。
  • 然后再构建代理,封装 invoker 返回服务引用,之后 Comsumer 调用的就是这个代理类。
  • 期间会包含 NettyClient,来进行远程通信,最后通过 Cluster 来包装 Invoker,默认是 FailoverCluster,最终返回代理类。

服务引入是 Consumer 从注册中心获取 Provider 地址,并创建代理对象的过程。

  1. Spring 容器启动
    • Consumer 通过 <dubbo:reference>@Reference 注解引用远程服务,Spring 容器解析配置,生成 ReferenceConfig
  2. 订阅服务
    • 向注册中心订阅目标服务,获取 Provider 的地址列表,并监听变更(动态扩缩容)。
    • 注册中心返回 Provider 的 URL 列表(如 dubbo://192.168.1.1:20880/service)。
  3. 生成 Invoker 链
    • 根据负载均衡策略(如 Random、RoundRobin)、集群容错模式(如 Failover、Failfast)等,将多个 Provider 地址封装为 Invoker 链。
    • 将 Provider URL 转换为 Invoker(如 DubboInvoker),每个 Provider 对应一个 Invoker。通过 Cluster 合并多个 Invoker,加入容错逻辑(如 FailoverCluster),形成单个集群容错 Invoker。
  4. 创建动态代理
    • 通过 ProxyFactory(默认 JavassistProxyFactory)生成服务接口的代理对象,代理对象内部调用 Invoker,屏蔽远程调用细节。
    • 注入代理对象:将代理对象注入到 Spring Bean 中,Consumer 像调用本地方法一样调用远程服务。
  5. 服务目录动态更新:监听注册中心,实时更新 Provider 列表(如 Provider 上线/下线)。触发路由规则过滤,更新可用 Invoker 列表。

问:服务调用的流程?Dubbo的服务调用基本原理是?

调用某个接口的方法会调用之前生成的代理类,然后会从 cluster 中经过路由的过滤、负载均衡机制选择一个 invoker 发起远程调用,此时会记录此请求和请求的 ID 等待服务端的响应。

服务端接受请求之后会通过参数找到之前暴露存储的 map,得到相应的 exporter ,然后最终调用真正的实现类,再组装好结果返回,这个响应会带上之前请求的 ID。

消费者收到这个响应之后会通过 ID 去找之前记录的请求,然后找到请求之后将响应塞到对应的 Future 中,唤醒等待的线程,最后消费者得到响应,一个流程完毕。

关键的就是 cluster、路由、负载均衡,然后 Dubbo 默认是异步的,所以请求和响应是如何对应上的。

  1. 消费者调用代理对象(Stub),实际调用的是 Invoker

    通过 Cluster 选择 Invoker(可能有多个服务提供者)。

    执行负载均衡策略(LoadBalance),选取一个合适的服务提供者。

    序列化请求,通过 Codec 进行编码。

    网络传输(Netty/HTTP),请求发送到 Provider。

    Provider 端解码请求,查找对应的 Invoker 并调用目标方法。

    执行业务逻辑,并返回结果。

    Provider 端序列化返回结果,并通过网络传输给 Consumer。

    Consumer 端接收并反序列化结果,返回给调用者。

  1. 代理对象发起调用
  • Consumer 调用接口方法,触发代理对象的 InvocationHandler
  • 封装调用信息为 RpcInvocation(方法名、参数类型、参数值等)。
  1. 过滤链处理(Filter Chain)
  • 经过 Consumer 端 Filter 链(如监控、日志、上下文传递等)。
  1. 路由与负载均衡
  • 从服务目录(Directory)获取所有可用 Invoker。
  • 通过 Router 过滤不符合条件的 Invoker(如 IP 规则、标签路由)。
  • 使用 LoadBalance(如随机、轮询)选择一个 Invoker。
  1. 远程通信(DubboProtocol)
  • 将调用请求序列化(默认 Hessian2)。
  • 通过 Netty 等网络框架发送请求到目标 Provider。
  1. Provider 处理请求
  • Provider 反序列化请求,根据接口和方法名找到本地实现类。
  • 执行本地方法,返回结果。
  1. 结果响应与容错
  • 结果返回 Consumer 端,反序列化后处理。
  • 若调用失败,根据集群容错策略(如 Failover)重试其他 Provider。

问:SPI是什么?SPI使用场景?如何使用,如何实现?Dubbo为什么要自行实现SPI?

  • 什么是SPI?

    • SPI (Service Provider Interface),是一种用于实现模块化和可插拔设计的机制,实现服务的动态加载和发现。
    • 约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名
  • 使用场景?

    • JDBC驱动加载: 加载不同的 JDBC 驱动,通过 SPI 机制加载。
    • 日志框架: 不同的日志框架(如 Log4j、Logback)实现不同的日志功能,通过 SPI 机制加载。
    • 框架扩展: 许多框架(如 Spring、Dubbo)都使用 SPI 机制来扩展功能。
  • SPI的核心组成:核心工具类ServiceLoader

    • 服务接口(Service Interface):定义服务的规范或契约,其他模块提供具体实现。
    • 服务提供者(Service Provider):具体实现服务接口的类。
    • 配置文件:在META-INF/services目录下创建一个以服务接口全限定名为文件名的配置文件,文件中列出服务提供者的全限定类名。
    • 服务加载器(ServiceLoader):Java内置的ServiceLoader类用于动态加载和管理服务实现。
  • 示例:

    1. 定义服务接口

      1
      2
      3
      public interface PaymentService {
      void processPayment(double amount);
      }
    2. 实现服务提供者

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class PaypalPaymentService implements PaymentService {
      @Override
      public void processPayment(double amount) {
      System.out.println("Processing payment with PayPal: " + amount);
      }
      }

      // StripePaymentService.java
      public class StripePaymentService implements PaymentService {
      @Override
      public void processPayment(double amount) {
      System.out.println("Processing payment with Stripe: " + amount);
      }
      }
    3. 创建配置文件:在META-INF/services目录下,创建一个名为com.example.PaymentService的文件,其中列出所有实现类的全限定类名:

      1
      2
      com.example.PaypalPaymentService
      com.example.StripePaymentService
    4. 使用ServiceLoader加载服务,在代码中使用ServiceLoader动态加载和使用服务:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      import java.util.ServiceLoader;

      public class PaymentProcessor {
      public static void main(String[] args) {
      ServiceLoader<PaymentService> serviceLoader = ServiceLoader.load(PaymentService.class);

      for (PaymentService service : serviceLoader) {
      service.processPayment(100.0);
      }
      }
      }
  • ServiceLoader如何动态加载类?

    1. ServiceLoader#load作为入口,首先查找当前线程绑定的ClassLoader,默认使用SystemClassLoader。并创建LazyIterator。
    2. 调用ServiceLoader#iterator获取迭代器,在迭代中加载配置文件解析内容,得到实现类的全限定类名,并创建其对象实例放入到缓存中。
  • 为什么 Dubbo 不用 JDK 的 SPI,而是要自己实现?

    • SPI会遍历配置文件,将所有类实例化,当类初始化比较耗时的情况下无法按需加载。
  • Dubbo的SPI机制是怎样的?

    Dubbo的SPI中配置文件里面存放的是键值对,通过名字去文件里面找到对应的实现类全限定名然后加载实例化,按需加载。增加了 IOC AOP 的特性,还有自适应扩展机制

    配置文件目录:

    • META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
    • META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。
    • META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。

    示例:

    • 配置文件:

      1
      2
      Paypal = com.example.PaypalPaymentService
      Stripe = com.example.StripePaymentService
    • 接口:

      1
      2
      3
      4
      @SPI(FailoverCluster.NAME)
      public interface PaymentService {
      void processPayment(double amount);
      }
    • 实现:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class PaymentProcessorTest {
      @Test
      public void payment() throws Exception {
      Extensionloader<PaymentService> serviceLoader = Extensionloader.getExtensionloader(PaymentService.class);
      PaymentService paypal = serviceLoader.getExtension("Paypal");
      paypal.processPayment();
      PaymentService stripe = serviceLoader.getExtension("Stripe");
      stripe.processPayment();
      }
      }

    ExtensionLoader 类似 Java SPI的ServiceLoader。通过Extensionloader.getExtensionloader进行初始化,调用入口为ExtensionLoader.getExtension(name)。通过参数名来查找缓存中是否已有实例对象,没有则通过反射新建实例,执行set方法依赖注入。

  • Dubbo 为什么默认用 Javassist?引申 JDK 的动态代理、ASM、CGLIB。

    • Dubbo 用 Javassist 动态代理,就是快,且字节码生成方便。ASM 比 Javassist 更快,但是没有快一个数量级,而Javassist 只需用字符串拼接就可以生成字节码,而 ASM 需要手工生成,成本较高,比较麻烦。

问:Dubbo的服务发现是?

Dubbo 提供的是一种 Client-Based 的服务发现机制,依赖第三方注册中心组件来协调服务发现过程,支持常用的注册中心如 Nacos、Consul、Zookeeper 等。

  • 服务提供者Provider:注册 URL 地址到注册中心。
  • 服务消费者Consumer:从注册中心读取地址列表并订阅变更。
  • 注册中心:负责对数据进行聚合,每当地址列表发生变化,注册中心将最新的列表通知到所有订阅的消费者实例。

其中:

  1. Dubbo3提供了面向百万实例集群的服务发现机制,Dubbo3的注册中心以应用为粒度聚合实例数据,避免全量订阅带来性能损耗。每个消费服务的实例从注册中心订阅实例地址列表,相比于一些产品直接将注册中心的全量数据 (应用 + 实例地址) 加载到本地进程,Dubbo 实现了按需精准订阅地址信息。比如一个消费者应用依赖 app1、app2,则只会订阅 app1、app2 的地址列表更新,大幅减轻了冗余数据推送和解析的负担。Dubbo2 版本注册中心以服务粒度聚合实例地址,比应用粒度更细,也就意味着传输的数据量更大,因此在大规模集群下也遇到一些性能问题。注册中心负责以应用名 (dubbo.application.name) 对整个集群的实例地址进行聚合,每个对外提供服务的实例将自身的应用名、实例ip:port 地址信息 (通常还包含少量的实例元数据,如机器所在区域、环境等) 注册到注册中心。
  2. Dubbo SDK 在实现上对消费端地址列表处理过程做了大量优化,地址通知增加了异步、缓存、bitmap 等多种解析优化,避免了地址更新常出现的消费端进程资源波动。
  3. 在功能丰富度和易用性上,服务发现除了同步 ip、port 等端点基本信息到消费者外,Dubbo 还将服务端的 RPC/HTTP 服务及其配置的元数据信息同步到消费端,这让消费者、提供者两端的更细粒度的协作成为可能,Dubbo 基于此机制提供了很多差异化的治理能力。除了与注册中心的交互,Dubbo3 的完整地址发现过程还有一条额外的元数据通路,我们称之为元数据服务 (MetadataService),实例地址与元数据共同组成了消费者端有效的地址列表。消费者从注册中心接收到地址 (ip:port) 信息,然后与提供者建立连接并通过元数据服务读取到对端的元数据配置信息,两部分信息共同组装成 Dubbo 消费端有效的面向服务的地址列表。以上两个步骤都是在实际的 RPC 服务调用发生之前。
  4. Dubbo 服务发现扩展了多种注册中心组件支持,如 Nacos、Zookeeper、Consul、Redis、kubernetes 等,可以通过配置切换不同实现。还支持一个应用内配置多注册中心的情形如双注册、双订阅等,这对于实现不同集群地址数据互通、集群迁移等场景非常有用处。

问:Invoker、Directory和Cluster

1. Invoker:服务调用的最小执行单元

  • 定义
    Invoker 是 Dubbo 的核心模型,代表一个 可执行的服务调用对象。无论是本地调用还是远程调用,均被抽象为 Invoker

  • 类型

    • 本地 Invoker:直接调用同一 JVM 内的服务实现类(如 InjvmInvoker)。
    • 远程 Invoker:封装网络通信细节,调用远程服务节点(如 DubboInvoker)。
  • 职责

    • 封装调用目标信息(接口、方法、参数、地址、协议等)。
    • 执行调用逻辑(通过 Protocol 实现具体通信,如 Dubbo、HTTP)。
  • 示例代码

    1
    2
    3
    4
    public interface Invoker<T> {
    Result invoke(Invocation invocation) throws RpcException;
    // 其他方法(如获取接口、URL等)
    }

2. Directory:动态服务目录

  • 定义
    Directory 维护服务的 可用提供者列表,动态监听注册中心变化,及时更新 Invoker 列表。

  • 核心功能

    • 服务发现:从注册中心(如 Zookeeper)订阅服务,获取 Provider 地址列表。
    • 动态更新:监听注册中心节点变更(上下线),实时刷新 Invoker 列表。
    • 路由过滤:结合路由规则(如标签路由),筛选符合条件的 Invoker。
  • 实现类

    • RegistryDirectory:与注册中心交互,管理 Invoker 的生命周期。
  • 协作流程

    1
    注册中心通知 Provider 变更 → Directory 更新 Invoker 列表 → Cluster 获取最新列表进行调用

3. Cluster:集群容错与负载均衡

  • 定义
    Cluster 封装了 多节点调用的容错策略负载均衡逻辑,将多个 Invoker 伪装成单个 Invoker,对外暴露统一调用入口。

  • 核心功能

    • 负载均衡:通过 LoadBalance 策略(如随机、轮询)选择 Invoker。
    • 容错处理:根据策略(如 Failover、Forking)处理调用失败。
    • 路由增强:支持分组聚合、条件路由等高级特性。
  • 实现类

    • FailoverCluster:失败自动切换(默认)。
    • FailfastCluster:快速失败。
    • ForkingCluster:并行调用多个节点。
  • 协作流程

    1
    Consumer 发起调用 → Cluster 从 Directory 获取 Invoker 列表 → 应用负载均衡和容错策略 → 调用目标 Invoker

三者的协作关系

  1. 服务发现阶段
    • Directory 从注册中心获取 Provider 地址列表,生成对应的远程 Invoker
  2. 调用准备阶段
    • Cluster 将多个 Invoker 封装为一个集群 Invoker,集成负载均衡和容错逻辑。
  3. 调用执行阶段
    • 消费者调用集群 Invoker 的 invoke() 方法。
    • Cluster 通过 LoadBalance 选择具体 Invoker,执行调用,处理异常。

架构图示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+-------------------+       +-------------------+       +-------------------+
| Consumer | | Cluster | | Directory |
| | | | | |
| +-------------+ | | +-------------+ | | +-------------+ |
| | Proxy | | 调用 | | 容错策略 | | 获取 | | Invoker列表 | |
| | (代理对象) | ------> | | (如Failover)| | <---- | | (动态更新) | |
| +-------------+ | | +-------------+ | | +-------------+ |
+-------------------+ +-------------------+ +-------------------+
| 选择 Invoker
v
+-------------------+
| Invoker |
| (执行远程/本地调用) |
+-------------------+

总结

  • Invoker:服务调用的执行者,屏蔽底层通信细节。
  • Directory:动态维护服务提供者列表,保证调用目标的实时性。
  • Cluster:集成容错与负载均衡策略,提升服务调用的可靠性和效率。

整个过程发生在Cluster中:服务引入时,将多个远程调用都塞入服务目录Directory中,然后通过Cluster封装该目录同时提供各种容错功能,如FailOver、FailFast等,最终只暴漏给Consumer一个Invoker。消费者调用时得到目录中的Invoker列表,通过路由的过滤,得到这些Invokers后再由loadBalance来进行负载均衡选择一个Invoker最终发起调用。

Dubbo 将能调用的服务的对象都封装成 invoker。有ClusterInvoker ,封装了服务引入生成的 invoker 们,赋予其集群容错等能力,这个 invoker 就是暴露给消费者调用的。

集群容错是消费者端实现的。

服务目录是什么?Directory,是一群invoker的集合。可以通过目录来查找远程服务,集群新增一个Provider时,对应目录会添加一个invoker。所以目录实现了监听功能RegistryDirectory 。

RegistryDirectory 的三个作用:

  1. 获取invoker列表:通过doList方法,对方法名进行过滤,找到方法名对应的invokers。
  2. 监听注册中心的变化:通过实现NotifyListener接口感知注册中心的数据变更。
  3. 刷新invokers:监听变更的invokerUrls,调用refreshInvoker(invokerUrls),根据配置更新invokers。

还有一个StaticDirectory是用于多注册中心的时候,它是一个静态目录,即固定的不会增减的,所有 Invoker 是通过构造器来传入。

  • 单注册中心时,一条reference可能对应多个provider,生成多个invoker,将其存入RegistryDirectory中管理,对外通过一个invoker封装代替多invoker的情况。
  • 多注册中心时,会有多个已封装的invoker,通过StaticDirectory存入这些invoker,再封装起来只对外暴露一个invoker。之所以是静态的是因为多注册中心是写在配置里面的,不像服务可以动态变更。

服务目录经过路由规则过滤后,Consumer如何选择调用哪个invoker,若invoker调用失败怎么办?

  • Cluster会将一群invoker封装为一个clusterInovker,对于内部的invoker调用失败时还可以切换另一个。
    • Dubbo内部有多种Cluster实现,比如FailoverCluster,返回的是FailoverClusterInvoker,实现了失败自动切换功能,有一定的重试次数。
    • FailfastClusterInvoker则只会进行一次远程调用,失败后立即抛出异常,即快速失败。
    • FailsafeClusterInvoker是一种失败安全的cluster,调用出错只会记录日志,返回一个空结果,适合于写入审计日志等操作。
    • FailbackClusterInvoker会在调用失败后,记录下此次调用,返回一个空结果,并通过定时任务对失败的调用重试。适合于执行消息通知等最大努力场景。
    • ForkingClusterInvoker会在运行时把所有invoker通过线程池并发调用,只要有一个provider成功返回了结果就会立即停止。适用于对实时性要求比较高的场景。
    • BroadcastClusterInvoker会在运行时将所有invoker逐个调用,在最后判断只要有一个调用出错就抛出异常。适合通知所有提供者更新缓存或日志等本地资源信息的场景。
    • AbstractClusterInvoker是其余ClusterInvoker的父类,对应AvailableCluster,比较简单,适用于多注册中心场景。

三. 集群容错、服务路由、服务重试、流量管控

问:集群容错策略?failover,failfast?Dubbo的服务容错基本原理是?

1. Failover(失败自动切换)

  • 定义:默认策略。调用失败后,自动切换到其他 Provider 节点重试(可配置重试次数)。

  • 适用场景读操作(天然幂等)或 对数据一致性要求不敏感 的场景。

  • 配置示例

    1
    <dubbo:reference interface="com.example.UserService" retries="2" cluster="failover"/>
  • 流程

    1. 调用 Provider A 失败(如超时、异常)。
    2. 自动选择下一个 Provider B 重试,直到成功或达到最大重试次数。

2. Failfast(快速失败)

  • 定义:调用失败后立即报错,不进行重试。

  • 适用场景非幂等操作(如写操作),避免重复提交导致数据不一致。

  • 配置示例

    1
    <dubbo:reference interface="com.example.OrderService" cluster="failfast"/>
  • 流程

    1. 调用 Provider A 失败。
    2. 直接抛出异常,由业务代码处理。

3. Failsafe(失败安全)

  • 定义:调用失败后忽略异常,仅打印日志,返回空结果。

  • 适用场景日志记录监控上报 等可容忍失败的场景。

  • 配置示例

    1
    <dubbo:reference interface="com.example.LogService" cluster="failsafe"/>
  • 流程

    1. 调用 Provider A 失败。
    2. 记录错误日志,返回 null 或默认值,不中断主流程。

4. Failback(失败自动恢复)

  • 定义:调用失败后记录请求,后台定时重试(直到成功)。

  • 适用场景消息通知异步任务 等允许延迟处理的场景。

  • 配置示例

    1
    <dubbo:reference interface="com.example.NotificationService" cluster="failback"/>
  • 流程

    1. 调用 Provider A 失败。
    2. 将请求加入重试队列,定时(默认 5s)重试。
    3. 重试成功则移除队列,失败则持续重试。

5. Forking(并行调用)

  • 定义:同时调用多个 Provider 节点,只要有一个成功即返回结果。

  • 适用场景实时性要求高资源充足 的场景(如金融交易验证)。

  • 配置示例

    1
    <dubbo:reference interface="com.example.PaymentService" cluster="forking" forks="2"/>
  • 流程

    1. 并行调用 Provider A、Provider B。
    2. 任一节点返回成功结果,立即返回给 Consumer。
    3. 其他未完成的调用自动取消。

6. Broadcast(广播调用)

  • 定义:广播调用所有 Provider 节点,任意一个节点失败则整体失败。

  • 适用场景批量通知(如更新本地缓存、刷新配置)。

  • 配置示例

    1
    <dubbo:reference interface="com.example.CacheService" cluster="broadcast"/>
  • 流程

    1. 调用所有 Provider 节点。
    2. 若任一节点失败,抛出异常。

策略对比与选型建议

策略 重试机制 适用场景 资源消耗 数据一致性
Failover 自动切换重试 读操作、幂等操作 最终一致
Failfast 不重试 非幂等写操作 强一致
Failsafe 不重试,忽略异常 日志、监控等旁路操作 不保证
Failback 后台定时重试 异步任务、消息通知 最终一致
Forking 并行调用,取最先成功 高实时性场景(如交易验证) 强一致
Broadcast 全部调用,任一失败即失败 批量通知(如刷新缓存) 强一致

配置方式

1. 全局配置(服务提供方或消费方)

1
2
3
4
5
<!-- 服务提供方默认容错策略 -->
<dubbo:provider cluster="failover"/>

<!-- 服务消费方默认容错策略 -->
<dubbo:consumer cluster="failover"/>

2. 单个服务指定

1
<dubbo:reference interface="com.example.UserService" cluster="failfast"/>

3. 注解配置

1
2
@Reference(cluster = "failover")
private UserService userService;

源码实现要点

Dubbo 的容错策略通过 Cluster 接口实现,核心类为 AbstractClusterInvoker

1
2
3
public interface Cluster {
<T> Invoker<T> join(Directory<T> directory) throws RpcException;
}
  • 策略入口Cluster 实现类(如 FailoverCluster)将多个 Invoker 封装为一个集群容错的 Invoker
  • 负载均衡:容错策略通常与 LoadBalance 策略(如随机、轮询)结合使用。

实际应用案例

  1. 支付交易验证(Forking)
    调用多个风控服务节点,任一节点通过即放行交易。
  2. 订单创建(Failfast)
    避免因重试导致重复创建订单。
  3. 缓存刷新(Broadcast)
    通知所有服务节点刷新本地缓存。

问:Dubbo的服务路由是?

  1. 什么是服务路由?

    • 服务路由就是根据特定的路由规则,将请求定向到适当的服务实例,即consumer调用哪个provider。
  2. 路由规则有哪些?

    • 条件路由 ConditionRouter:使用表达式来选择路由。格式为[服务消费者匹配条件] => [服务提供者匹配条件],比如 host = 10.20.153.10 => host = 10.20.153.11

    • 脚本路由 ScriptRouter:使用动态脚本来定义路由规则,支持如Groovy、JavaScript等脚本语言。

      1
      2
      3
      4
      // 定义路由逻辑,根据方法名和参数进行路由。只有在调用getUser方法且参数为"admin"时,才会进行特定的路由。
      rule = '''function route() {
      return invocation.methodName == "getUser" && invocation.arguments[0] == "admin";
      }'''
    • 标签路由 TagRouter:基于标签(Tag)进行流量控制,允许将服务实例打上标签,然后根据这些标签来路由流量。

      假设某服务有两个标签:stablebeta,想要将流量按照一定比例分配:

      1
      2
      3
      4
      5
      6
      // 标签路由配置示例
      - consumer.label: stable
      provider.label: stable

      - consumer.label: beta
      provider.label: beta

      在上述配置中,稳定版本的消费者将被路由到稳定版本的提供者,而测试版本的消费者将被路由到测试版本的提供者。

  3. 路由的配置一样是通过 RegistryDirectory 的 notify 更新和构造的,然后路由的调用在是刷新 invoker 的时候,具体是在调用 toMethodInvokers 的时候会进行服务级别的路由和方法级别的路由。

问:Dubbo的负载均衡是?Dubbo有哪几种负载均衡策略,默认是哪种?

(1):随机调用{默认}

(2):权重轮询

(3):最少活跃数

(4):一致性Hash

  1. 什么是负载均衡?与服务路由的关系是?

    • 负载均衡是指在多个服务实例之间分配请求,以实现负载的均衡分布。它主要关注的是“如何将请求均匀地分配到多个实例”的问题。负载均衡确保了系统的高可用性和高性能。
    • 负载均衡分为硬件和软件两类,软件如Nginx,Dubbo的负载均衡为LoadBalance。负载均衡是用来选择出合适的服务提供者给消费者调用的。
    • 服务路由是指根据特定的路由规则,将请求定向到适当的服务实例或服务集群。它主要关注的是“将请求发送到哪个服务或实例”的问题。服务路由在请求路径、服务发现、版本控制等场景中起作用。
    • 二者经常组合使用,根据收到请求后由路由规则匹配到合适的服务集群,再通过负载均衡算法将请求分配到一个具体的实例。
  2. Dubbo有哪些负载均衡策略?Dubbo 提供的是客户端负载均衡,即由 Consumer 通过负载均衡算法得出需要将请求提交到哪个 Provider 实例。默认为(weighted random) 基于权重的随机负载均衡

    包括:

    算法 特性 备注
    Weighted Random LoadBalance 加权随机 默认算法,默认权重相同
    RoundRobin LoadBalance 加权轮询 借鉴于 Nginx 的平滑加权轮询算法,默认权重相同,
    LeastActive LoadBalance 最少活跃优先 + 加权随机 背后是能者多劳的思想
    Shortest-Response LoadBalance 最短响应优先 + 加权随机 更加关注响应速度
    ConsistentHash LoadBalance 一致性哈希 确定的入参,确定的提供者,适用于有状态请求
    P2C LoadBalance Power of Two Choice 随机选择两个节点后,继续选择“连接数”较小的那个节点。
    Adaptive LoadBalance 自适应负载均衡 在 P2C 算法基础上,选择二者中 load 最小的那个节点

    具体为:

    • Weighted Random
      • 加权随机,按权重设置随机概率。在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
      • 缺点:存在慢的提供者累积请求的问题,比如:集群有多台机器,别的机器能正常处理请求,但第二台机器很慢,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。轮询、随机的负载均衡策略都会遇到该问题。
    • RoundRobin

      • 加权轮询,按公约后的权重设置轮询比率,循环调用节点
      • 缺点:同样存在慢的提供者累积请求的问题。

      加权轮询过程中,如果某节点权重过大,会存在某段时间内调用过于集中的问题。 例如 ABC 三节点有如下权重:{A: 3, B: 2, C: 1} 那么按照最原始的轮询算法,调用过程将变成:A A A B B C

      对此,Dubbo 借鉴 Nginx 的平滑加权轮询算法,对此做了优化,调用过程可抽象成下表:

      轮前加和权重 本轮胜者 合计权重 轮后权重(胜者减去合计权重)
      起始轮 \ \ A(0), B(0), C(0)
      A(3), B(2), C(1) A 6 A(-3), B(2), C(1)
      A(0), B(4), C(2) B 6 A(0), B(-2), C(2)
      A(3), B(0), C(3) A 6 A(-3), B(0), C(3)
      A(0), B(2), C(4) C 6 A(0), B(2), C(-2)
      A(3), B(4), C(-1) B 6 A(3), B(-2), C(-1)
      A(6), B(0), C(0) A 6 A(0), B(0), C(0)

      我们发现经过合计权重(3+2+1)轮次后,循环又回到了起点,整个过程中节点流量是平滑的,且哪怕在很短的时间周期内,概率都是按期望分布的。

      如果用户有加权轮询的需求,可放心使用该算法。

    • LeastActive

      • 加权最少活跃调用优先,活跃数越低,越优先调用,相同活跃数的进行加权随机。活跃数指调用前后计数差(针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供者处理能力越强。
      • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大;相对的,处理能力越强的节点,处理更多的请求。
    • ShortestResponse

      • 加权最短响应优先,在最近一个滑动窗口中,响应时间越短,越优先调用。相同响应时间的进行加权随机。
      • 使得响应时间越快的提供者,处理更多的请求。
      • 缺点:可能会造成流量过于集中于高性能节点的问题。

      这里的响应时间 = 某个提供者在窗口时间内的平均响应时间,窗口时间默认是 30s。

    • ConsistentHash

      • 一致性 Hash,相同参数的请求总是发到同一提供者。
      • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
      • 算法参见:Consistent Hashing | WIKIPEDIA 通过TreeMap实现哈希圆环, 通过哈希值查找大于等于的invoker,若没有则返回第一个。
      • 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
      • 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />
    • P2C Load Balance

      Power of Two Choice 算法简单但是经典,主要思路如下:

      1. 对于每次调用,从可用的provider列表中做两次随机选择,选出两个节点providerA和providerB。
      2. 比较providerA和providerB两个节点,选择其“当前正在处理的连接数”较小的那个节点。

      以下是 Dubbo P2C 算法实现提案

    • Adaptive Load Balance

      Adaptive 即自适应负载均衡,是一种能根据后端实例负载自动调整流量分布的算法实现,它总是尝试将请求转发到负载最小的节点。

      以下是 Dubbo Adaptive 算法实现提案

问:Dubbo的流量管控是?限流和熔断?

Dubbo 提供的流量管控策略:

  • 地址发现与负载均衡,地址发现支持服务实例动态上下线,负载均衡确保流量均匀的分布到每个实例上。
  • 基于路由规则的流量管控,路由规则对每次请求进行条件匹配,并将符合条件的请求路由到特定的地址子集。

Dubbo 的流量管控规则可以基于应用、服务、方法、参数等粒度精准的控制流量走向,根据请求的目标服务、方法以及请求体中的其他附加参数进行匹配,符合匹配条件的流量会进一步的按照特定规则转发到一个地址子集。流量管控规则有以下几种:

  • 标签路由规则:通过将某一个服务的实例划分到不同的分组,约束具有特定标签的流量只能在指定分组中流转,不同分组为不同的流量场景服务,从而实现流量隔离的目的。标签路由可以作为蓝绿发布、灰度发布等场景能力的基础。

    标签路由规则是一个非此即彼的流量隔离方案,也就是匹配标签的请求会 100% 转发到有相同标签的实例,没有匹配标签的请求会 100% 转发到其余未匹配的实例。如果您需要按比例的流量调度方案,请参考示例 基于权重的按比例流量路由

    标签主要是指对 Provider 端应用实例的分组,目前有两种方式可以完成实例分组,分别是动态规则打标静态规则打标动态规则打标 可以在运行时动态的圈住一组机器实例,而 静态规则打标 则需要实例重启后才能生效,其中,动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。

    示例:

    1. 静态打标:需要在服务提供者实例启动前确定,并且必须通过特定的参数 tag 指定。

      Provider

      1
      2
      3
      4
      5
      6
      在 Dubbo 实例启动前,指定当前实例的标签,如部署在杭州区域的实例,指定 tag=gray。
      <dubbo:provider tag="gray"/>
      or
      <dubbo:service tag="gray"/>
      or
      java -jar xxx-provider.jar -Ddubbo.provider.tag=gray

      Consumer

      1
      2
      发起调用的一方,在每次请求前通过 tag 设置流量标签,确保流量被调度到带有同样标签的服务提供方。
      RpcContext.getContext().setAttachment(Constants.TAG_KEY, "gray");
    2. 动态打标:相比于静态打标只能通过 tag 属性设置,且在启动阶段就已经固定下来,动态标签可以匹配任意多个属性,根据指定的匹配条件将 Provider 实例动态的划分到不同的流量分组中。

      Provider

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      # 以下规则对 shop-detail 应用进行了动态归组,匹配 env: gray 的实例被划分到 gray 分组,其余不匹配 env: gray 继续留在默认分组 (无 tag)。
      configVersion: v3.0
      force: true
      enabled: true
      key: shop-detail
      tags:
      - name: gray
      match:
      - key: env
      value:
      exact: gray

      Consumer

      1
      2
      服务发起方的设置方式和之前静态打标规则保持一致,只需要在每次请求前通过 tag 设置流量标签,确保流量被调度到带有同样标签的服务提供方。
      RpcContext.getContext().setAttachment(Constants.TAG_KEY, "Hangzhou");

    设置了以上标签的流量,将全部导流到 hangzhou-region 划分的实例上。请求标签的作用域仅为一次点对点的 RPC 请求。比如,在一个 A -> B -> C 调用链路上,如果 A -> B 调用通过 setAttachment 设置了 tag 参数,则该参数不会在 B -> C 的调用中生效,同样的,在完成了 A -> B -> C 的整个调用同时 A 收到调用结果后,如果想要相同的 tag 参数,则在发起其他调用前仍需要单独设置 setAttachment

  • 条件路由规则

    条件路由与标签路由的工作模式非常相似,也是首先对请求中的参数进行匹配,符合匹配条件的请求将被转发到包含特定实例地址列表的子集。相比于标签路由,条件路由的匹配方式更灵活:

    • 在标签路由中,一旦给某一台或几台机器实例打了标签,则这部分实例就会被立马从通用流量集合中移除,不同标签之间不会再有交集。有点类似下图,地址集合在输入阶段就已经划分明确。

    tag-condition-compare

    • 而从条件路由的视角,所有的实例都是一致的,路由过程中不存在分组隔离的问题,每次路由过滤都是基于全量地址中执行

    tag-condition-compare

    条件路由规则的主体 conditions 主要包含两部分内容:

    • => 之前的为请求参数匹配条件,指定的 匹配条件指定的参数 将与 消费者的请求上下文 (URL)、甚至方法参数 进行对比,当消费者满足匹配条件时,对该消费者执行后面的地址子集过滤规则。
    • => 之后的为地址子集过滤条件,指定的 过滤条件指定的参数 将与 提供者实例地址 (URL) 进行对比,消费者最终只能拿到符合过滤条件的实例列表,从而确保流量只会发送到符合条件的地址子集。
      • 如果匹配条件为空,表示对所有请求生效,如:=> status != staging
      • 如果过滤条件为空,表示禁止来自相应请求的访问,如:application = product =>

    示例:

    基于以下示例规则,所有 org.apache.dubbo.demo.CommentService 服务调用都将被转发到与当前消费端机器具有相同 region 标记的地址子集。$region 是特殊引用符号,执行过程中将读取消费端机器的实际的 region 值替代。

    1
    2
    3
    4
    5
    6
    configVersion: v3.0
    enabled: true
    force: false
    key: org.apache.dubbo.samples.CommentService
    conditions:
    - '=> region = $region'

    条件路由规则还支持设置具体的机器地址如 ip 或 port,这种情况下使用条件路由可以处理一些开发或线上机器的临时状况,实现黑名单、白名单、实例临时摘除等运维效果,如以下规则可以将机器 172.22.3.91 从服务的可用列表中排除 => host != 172.22.3.91。条件路由还支持基于请求参数的匹配:

    1
    2
    conditions:
    - method=getDetail&arguments[0]=dubbo => port=20880
  • 动态配置规则

    通过 Dubbo 提供的动态配置规则,您可以动态的修改 Dubbo 服务进程的运行时行为,整个过程不需要重启,配置参数实时生效。基于这个强大的功能,基本上所有运行期参数都可以动态调整,比如超时时间、临时开启 Access Log、修改 Tracing 采样率、调整限流降级参数、负载均衡、线程池配置、日志等级、给机器实例动态打标签等。与上文讲到的流量管控规则类似,动态配置规则支持应用、服务两个粒度,也就是说您一次可以选择只调整应用中的某一个或几个服务的参数配置。

    当然,出于系统稳定性、安全性的考量,有些特定的参数是不允许动态修改的,但除此之外,基本上所有参数都允许动态修改,很多强大的运行态能力都可以通过这个规则实现,您可以找个示例应用去尝试一下。通常 URL 地址中的参数均可以修改,这在每个语言实现的参考手册里也记录了一些更详细的说明。

    示例:

    org.apache.dubbo.samples.UserService 服务的超时参数调整为 2000ms

    1
    2
    3
    4
    5
    6
    7
    8
    configVersion: v3.0
    scope: service
    key: org.apache.dubbo.samples.UserService
    enabled: true
    configs:
    - side: provider
    parameters:
    timeout: 2000

    以下部分指定这个配置是服务粒度,具体变更的服务名为 org.apache.dubbo.samples.UserServicescope 支持 serviceapplication 两个可选值,如果 scope: service,则 key 应该配置为 version/service:group 格式。

    1
    2
    scope: service
    key: org.apache.dubbo.samples.UserService

    parameters 参数指定了新的修改值,这里将通过 timeout: 2000 将超时时间设置为 2000ms。

    1
    2
    parameters:
    timeout: 2000
  • 脚本路由规则

    脚本路由是最直观的路由方式,同时它也是当前最灵活的路由规则,因为你可以在脚本中定义任意的地址筛选规则。如果我们为某个服务定义一条脚本规则,则后续所有请求都会先执行一遍这个脚本,脚本过滤出来的地址即为请求允许发送到的、有效的地址集合。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    configVersion: v3.0
    key: demo-provider
    type: javascript
    enabled: true
    script: |
    (function route(invokers,invocation,context) {
    var result = new java.util.ArrayList(invokers.size());
    for (i = 0; i < invokers.size(); i ++) {
    if ("10.20.3.3".equals(invokers.get(i).getUrl().getHost())) {
    result.add(invokers.get(i));
    }
    }
    return result;
    } (invokers, invocation, context)); // 表示立即执行方法

如果底层用的是基于 HTTP 的 RPC 协议 (如 REST、gRPC、Triple 等),则服务和方法等就统一映射为 HTTP 路径 (path),此时 Dubbo 路由规则相当于是基于 HTTP path 和 headers 的流量分发机制。

工作原理:

以下是 Dubbo 单个路由器的工作过程:路由器接收一个服务的实例地址集合作为输入,基于请求上下文 (Request Context) 和 (Router Rule) 实际的路由规则定义对输入地址进行匹配,所有匹配成功的实例组成一个地址子集,最终地址子集作为输出结果继续交给下一个路由器或者负载均衡组件处理。

Router

在 Dubbo 中,多个路由器组成一条路由链共同协作,前一个路由器的输出作为另一个路由器的输入,经过层层路由规则筛选后,最终生成有效的地址集合。

  • Dubbo 中的每个服务都有一条完全独立的路由链,每个服务的路由链组成可能不同,处理的规则各异,各个服务间互不影响。
  • 对单条路由链而言,即使每次输入的地址集合相同,根据每次请求上下文的不同,生成的地址子集结果也可能不同。

Router

限流 & 熔断?

  • 流量控制 (Rate Limiting):流量控制更多的是站在 Dubbo 服务提供者视角来保证服务稳定性,通过明确的为 Dubbo 服务设置请求上限阈值,确保服务所处理的请求数始终在一个合理范围之内,从而确保系统整体的稳定性。

    provider-rate-limit

    根据服务的具体部署情况,服务所能处理的流量上限是一定的,当对服务的请求数量保持在合理的范围时,系统运行正常;而当请求数量严重超过服务处理能力时,如大促期间的流量洪峰等场景,就可能造成服务提供者端的资源过度消耗、负载过高,进而出现响应延迟、请求无应答、系统假死等情况。

    流量控制解决的问题和工作方式比较容易理解,而其使用的难点就是如何确定服务所能处理的流量最大值?

  • 熔断降级 (Circuit Breaking):熔断降级则是更多的从 Dubbo 服务消费者视角来保障系统稳定性的重要手段。一个服务往往需要调用更多的下游 Dubbo 服务来完成业务逻辑,这时下游服务的稳定性就会影响当前服务甚至整个系统的稳定性,熔断(Circuit Breaking)即是面向不稳定服务场景设计的,它能最大限度避免下游服务不稳定对上游服务带来的影响。

    而相比于熔断后直接返回调用失败信息,配合服务降级能力,我们可以继续调用预先设置好的服务降级逻辑,以降级逻辑的结果作为最终调用结果,以更优雅的返回给服务调用方。

    consumer-circuit-breaking

    如上图所示,Dubbo Consumer 依赖的下游的三个 Dubbo 服务,当 Service 3 出现不稳定的情况时(如响应时间变长、错误率增加等),从而 Consumer 调用 Service 3 的线程等资源就会产生堆积,如果此时我们不在 Consumer 侧做任何限制,则 Service 1 与 Service 2 的调用都会受到稳定性影响。通过熔断 Service 3 我们就能保证整个 Dubbo Consumer 服务的稳定性,不拖垮整个 Consumer 服务,熔断 Service 3 的方式可以有很多种实现,包括线程数、信号量、错误率等。

    Dubbo 通过集成业界主流的框架实现了服务熔断降级能力

问:Dubbo的服务重试基本原理是?

服务重试机制 是 Dubbo 中的另一个关键功能,确保在调用失败或超时的情况下,能够通过重试机制提高服务调用的成功率。重试机制通常与容错机制结合使用。

  1. 失败重试:
    • 当服务调用出现异常(如超时、网络异常等)时,Dubbo 会根据配置的容错策略进行重试。对于一些失败可以被恢复的操作(如网络故障),Dubbo 会尝试重新发起请求。
  2. 重试次数:
    • 重试次数可以通过 retries 参数进行配置。retries 指定了最大重试次数,默认为 2 次(即总共 3 次尝试:1 次原始调用 + 2 次重试)。
    • Failover 策略下,Dubbo 会在服务调用失败时,自动切换到其他的可用服务提供者,并进行重试,直到重试次数用尽或者成功。
  3. 重试间隔:
    • 重试的间隔时间也可以进行配置,Dubbo 支持 固定间隔随机间隔 进行重试。
  4. 调用超时与重试:
    • 如果服务调用超过设定的 超时时间,Dubbo 会视为超时异常,并根据容错策略进行重试。对于 FailoverFailback 策略,会尝试不同的服务提供者;对于 Failfast 策略,则不会进行重试。

服务重试的实现机制

  • Proxy 代理:在 Consumer 调用 Provider 时,Dubbo 会通过动态代理生成代理对象。代理对象会拦截调用请求,检查是否需要重试。如果需要重试,代理对象会重新发起请求。
  • 异步请求:Dubbo 也支持异步调用模式,在异步模式下,重试请求会在后台执行,并通过回调处理最终结果。

四. 通信协议、序列化框架

问:Dubbo使用的是什么通信框架?Dubbo默认使用的是什么通信框架,还有哪些?

默认使用 Netty 框架,也是推荐的选择,另外内容还集成有Mina、Grizzly。

问:dubbo 支持的协议及序列化方式?Dubbo支持哪些协议,每种协议的应用场景,优缺点?Dubbo推荐使用什么序列化框架,还有哪些?

推荐使用Hessian序列化,还有Duddo、FastJson、Java自带序列化

1. Dubbo 协议(默认协议)

  • 特点:基于 Netty 的 NIO 通信框架,采用单一长连接和异步调用。
  • 默认端口20880
  • 序列化:Hessian2(默认)、Kryo、FST 等。
  • 应用场景
    • 内部服务调用:适用于高并发、低延迟的内部服务间通信。
    • 长连接复用:适合服务提供者与消费者数量差异较大的场景(如 Provider 少,Consumer 多)。
  • 优点
    • 高性能:基于 Netty 的异步非阻塞模型,传输效率高。
    • 轻量级:协议头小(16字节),适合高频调用。
  • 缺点
    • 跨语言支持弱:默认依赖 Java 生态,非 Java 语言需通过网关或 Sidecar 接入。
    • 穿透防火墙难:使用自定义端口(如 20880),可能被企业防火墙拦截。

2. HTTP 协议

  • 特点:基于 HTTP/1.1 协议,兼容 Spring Cloud 等标准 HTTP 服务。
  • 默认端口808080
  • 序列化:JSON、XML 等文本格式。
  • 应用场景
    • 跨语言调用:需要与非 Java 服务(如 Node.js、Python)交互。
    • 穿透防火墙:HTTP 协议通常被企业防火墙放行。
  • 优点
    • 通用性强:广泛支持各类语言和框架。
    • 调试方便:可通过浏览器或 Postman 直接调试接口。
  • 缺点
    • 性能较低:文本协议传输效率低于二进制协议(如 Dubbo 协议)。
    • 无长连接复用:每次请求需重新建立连接(可通过 HTTP/2 优化)。

3. Hessian 协议

  • 特点:基于 HTTP 的二进制协议,支持跨语言。
  • 默认端口80
  • 序列化:Hessian 序列化。
  • 应用场景
    • 跨语言服务调用:需与 PHP、.NET 等语言互通。
    • 替代原生 HTTP 协议:追求比 JSON/XML 更高的性能。
  • 优点
    • 跨语言支持:Hessian 协议天然支持多语言。
    • 性能优于 HTTP:二进制协议传输效率更高。
  • 缺点
    • 兼容性问题:不同语言的 Hessian 实现可能存在差异。
    • 扩展性弱:协议升级需各语言同步支持。

4. gRPC 协议

  • 特点:基于 HTTP/2 和 Protobuf 的高性能协议。
  • 默认端口50051
  • 序列化:Protobuf(默认)。
  • 应用场景
    • 微服务跨语言通信:需与 Go、Python 等语言交互。
    • 流式通信:支持双向流、客户端流、服务端流。
  • 优点
    • 高性能:HTTP/2 多路复用 + Protobuf 高效序列化。
    • 强类型约束:Protobuf 定义接口,减少通信错误。
  • 缺点
    • 生态依赖:需依赖 Protobuf 定义 IDL 文件。
    • 调试复杂度高:需工具生成和解析二进制数据。

5. Thrift 协议

  • 特点:Facebook 开源的跨语言 RPC 框架。
  • 默认端口9090
  • 序列化:Thrift 二进制格式。
  • 应用场景
    • 多语言混合架构:需与 C++、Ruby 等语言互通。
    • 高性能场景:追求极致传输效率。
  • 优点
    • 跨语言支持完善:官方提供多语言 SDK。
    • 协议紧凑:传输体积小,性能高。
  • 缺点
    • 维护成本高:需维护 Thrift IDL 文件。
    • 社区活跃度下降:逐渐被 gRPC 取代。

6. WebService (SOAP) 协议

  • 特点:基于 XML 的 SOAP 协议,支持 WSDL 描述。
  • 默认端口80
  • 序列化:XML。
  • 应用场景
    • 遗留系统集成:与老旧 SOAP 服务交互。
    • 需要 WSDL 的场景:通过 WSDL 生成客户端代码。
  • 优点
    • 标准化:协议规范严格,适合企业级集成。
    • 工具链成熟:支持 WSDL 生成和解析。
  • 缺点
    • 性能差:XML 序列化体积大,解析耗资源。
    • 复杂性高:SOAP 协议头冗余,开发效率低。

Dubbo 支持的序列化方式

序列化方式 特点 适用场景
Hessian2 跨语言、二进制序列化,性能较好(Dubbo 默认) 跨语言调用或默认场景
Kryo 高性能二进制序列化,仅限 Java,需注册类 纯 Java 高性能场景
FST 比 Kryo 更快的序列化,同样需注册类 极致性能优化的 Java 服务
JSON 文本格式,跨语言支持强,性能较低 调试、与非 Java 服务交互
Protobuf 高效二进制协议,需定义 IDL,跨语言支持好(gRPC 协议默认) 跨语言高性能通信
Java 原生 JDK 自带,兼容性好,性能差 兼容旧系统或简单测试

协议选型建议

场景 推荐协议 理由
纯 Java 高性能内部调用 Dubbo 协议 默认协议,基于 Netty 的异步高性能模型,适合高并发场景。
跨语言调用 gRPC/HTTP gRPC 提供高性能跨语言支持,HTTP 适合简单 RESTful 交互。
穿透企业防火墙 HTTP 80/443 端口通常开放,避免被拦截。
流式通信需求 gRPC 支持双向流、客户端流、服务端流,适合实时数据传输。
遗留系统集成 WebService 兼容 SOAP 协议,适合与老旧系统交互。

配置示例(XML 方式)

1
2
3
4
5
6
7
8
<!-- 使用 Dubbo 协议 -->
<dubbo:protocol name="dubbo" port="20880" serialization="kryo"/>

<!-- 使用 HTTP 协议 -->
<dubbo:protocol name="http" port="8080"/>

<!-- 使用 gRPC 协议 -->
<dubbo:protocol name="grpc" port="50051"/>

总结

Dubbo 的多协议支持使其能够灵活适配不同场景,开发者可根据 性能需求跨语言要求网络环境 选择合适的协议。对于内部 Java 服务,默认的 Dubbo 协议是最优选择;若需跨语言或穿透防火墙,可选用 HTTP/gRPC;而 Thrift 和 WebService 则适用于特定遗留系统集成场景。

五. 常见问题

问:服务调用超时问题怎么解决?Dubbo超时时间怎样设置?

服务调用超时问题解决方案:

服务调用超时通常是由于网络不稳定、服务端处理慢、资源压力过大等原因引起的。在 Dubbo 中,可以通过以下方式来解决服务调用超时问题:

  1. 合理设置超时时间
    • 如果超时时间设置过短,可能导致正常的服务调用也被视为超时,从而产生不必要的重试和异常。
    • 如果设置过长,则可能导致长时间等待,影响系统性能和用户体验。
    • 一般来说,超时时间应根据实际业务场景和服务的响应时间进行调整。
  2. 服务端优化
    • 服务端的性能优化是解决超时问题的根本,确保服务端有足够的资源来处理请求。
    • 可以通过优化代码、增加缓存、分布式限流等方式来提高服务的处理速度。
  3. 客户端优化
    • 客户端可以通过 重试机制 来容忍偶尔的超时,避免请求失败。
    • 配置 超时重试策略,比如设置重试次数和间隔时间。

Dubbo超时时间的设置

  • 在 Dubbo 中,超时时间是通过 timeout 配置项来设置的,单位为 毫秒。可以在消费者(Consumer)和服务提供者(Provider)端分别设置。

  • Consumer 端设置

    1
    2
    3
    4
    5
    xml


    CopyEdit
    <dubbo:reference id="userService" interface="com.example.UserService" timeout="3000" />

    或者通过注解的方式:

    1
    2
    javaCopyEdit@DubboReference(timeout = 3000)
    private UserService userService;
  • Provider 端设置

    1
    2
    3
    4
    5
    xml


    CopyEdit
    <dubbo:service id="userService" interface="com.example.UserService" timeout="5000" />
  • 全局设置: 在 Dubbo 的全局配置中设置超时时间:

    1
    2
    3
    4
    5
    xml


    CopyEdit
    <dubbo:protocol name="dubbo" timeout="5000" />

超时配置的注意事项

  • consumer.timeout:消费者端的超时时间,控制请求发出后等待服务端响应的最大时间。
  • provider.timeout:服务提供者端的超时时间,控制服务端处理请求的最大时间。如果超时,服务提供者将直接返回错误。

问:Dubbo在安全机制方面是如何解决的?

Dubbo 提供了以下几种安全机制来确保分布式服务调用的安全性:

  1. 身份认证与授权
    • 身份认证:确保客户端和服务端的身份合法。Dubbo 支持通过 token、证书等方式 进行身份验证,防止未经授权的访问。
    • 授权管理:对不同的服务提供不同的访问权限,确保只有有权限的客户端能够调用服务。
  2. 数据加密
    • Dubbo 支持对数据进行加密,避免敏感数据在传输过程中被窃取。
    • Dubbo 提供了 SSL/TLS 加密 来保护数据的机密性,防止信息泄漏。
  3. 安全协议
    • Dubbo 支持多种安全协议,如 SSL/TLS 加密协议,可以用于保护客户端和服务端之间的通信。
    • 使用安全的协议(如 HTTPS)来保证通信的加密性。
  4. 访问控制与流量控制
    • 访问控制:对服务的访问进行控制,只有符合条件的客户端才能访问服务。可以通过 IP 白名单黑名单 来限制服务的访问。
    • 流量控制:对访问进行流量限制和限速,防止恶意请求导致系统崩溃。
  5. 认证机制(基于 Token 或加密)
    • Dubbo 通过身份认证(例如基于 Token 的认证)和 加密机制(如使用公私钥对进行加密)来保证数据在网络传输中的安全性。

问:Dubbo的注册中心集群挂掉,发布者和订阅者之间还能通信么?

在 Dubbo 中,注册中心的作用是负责服务的注册与发现。当注册中心出现故障时,会对服务的发现和路由产生影响。具体影响如下:

  1. 注册中心挂掉对服务发现的影响
    • 如果 注册中心挂掉,服务消费者无法从注册中心获取到服务提供者的地址,导致服务发现失败,消费者无法调用服务。
    • 但是,已缓存的服务地址可能仍然有效,消费者可以通过缓存的服务地址进行调用,直到缓存失效。
  2. 服务的持续通信
    • 服务提供者与消费者之间的通信依赖于注册中心来进行服务的动态发现和更新。如果注册中心宕机,已建立的连接仍然可以继续通信,但是无法获取到新的服务地址或者注册的新服务。
    • 已缓存的服务信息:如果消费者和提供者在注册中心宕机前已经获取了服务信息(如服务地址、端口等),并且没有新的服务提供者加入或变更,通信可以继续进行,直到缓存过期。
  3. Dubbo的容错机制
    • 如果注册中心不可用,Dubbo 会尝试通过 failover、failback 等容错策略进行处理,尽量保证服务的可用性。
    • 例如,Dubbo 支持的 Failback 策略会在注册中心恢复后,自动进行服务重新注册和发现。

总结

  • 如果 注册中心挂掉,已经建立连接的消费者和服务提供者可以继续通信,但无法获取新的服务信息,直到注册中心恢复。Dubbo 提供了容错策略来减少注册中心不可用对服务的影响。

问:如果让你设计一个 RPC 框架,如何设计?

可以从底层向上开始说起

首先需要实现高性能的网络传输,可以采用 Netty 来实现,不用自己重复造轮子,然后需要自定义协议,毕竟远程交互都需要遵循一定的协议,然后还需要定义好序列化协议,网络的传输毕竟都是二进制流传输的。

然后可以搞一套描述服务的语言,即 IDL(Interface description language),让所有的服务都用 IDL 定义,再由框架转换为特定编程语言的接口,这样就能跨语言了。

此时最近基本的功能已经有了,但是只是最基础的,工业级的话首先得易用,所以框架需要把上述的细节对使用者进行屏蔽,让他们感觉不到本地调用和远程调用的区别,所以需要代理实现。

然后还需要实现集群功能,因此的要服务发现、注册等功能,所以需要注册中心,当然细节还是需要屏蔽的。

最后还需要一个完善的监控机制,埋点上报调用情况等等,便于运维。